#ifndef _FMODHandler_h_
#define _FMODHandler_h_

#include <fmod.hpp> // Awesome (free) sound processor
#include <fmod_errors.h> // String messages, not using

//============================================================================================================================
void ERRCHECK(FMOD_RESULT result)
{
    if (result != FMOD_OK)
    {
        Exclamation(Format("FMOD error! (%d) %s\n", result, FMOD_ErrorString(result)));
        exit(-1);
    }
}

//============================================================================================================================
void ERRLOG(FMOD_RESULT result, String origFilePath, const char *convertedFilePath)
{
    if (result != FMOD_OK)
    {
        String msg;
        String errstr = Format("%s", FMOD_ErrorString(result));
        msg.Cat() << "FMOD error (" << (int)result << ") " << errstr;
        if (convertedFilePath) {
            msg.Cat() << ", (" << convertedFilePath << "), (" << origFilePath << ")";
        }

    	LOG(msg);
    	Exclamation(msg);
    }
}

	
//============================================================================================================================
FMOD_RESULT F_CALLBACK opennonblocking(const char *name, int unicode, unsigned int *filesize, void **handle, void **userdata)
{
    if (name)
    {
		int fdesc = open(name, O_RDONLY|O_NONBLOCK); //Non-blocking, applies to open operation only
		if (fdesc == -1) {
			LOG(CAT << "opennonblocking: " << strerror(errno));
			return FMOD_ERR_FILE_NOTFOUND;
		}
		
		//fcntl(fdesc, F_SETFL, O_NONBLOCK); // <fcntl.h>	

        int lpos = lseek(fdesc, 0, SEEK_END);
        if (lpos < 0) {
            LOG(CAT << "opennonblocking: " << strerror(errno));
            return FMOD_ERR_INVALID_PARAM;
        }
		*filesize = lpos;
        lseek(fdesc, 0, SEEK_SET);
        *userdata = (void *)0x12345678;
        *handle = (void *)fdesc;
    }

    return FMOD_OK;
}

FMOD_RESULT F_CALLBACK closenonblocking(void *handle, void *userdata) {
    if (!handle)
    {
        return FMOD_ERR_INVALID_PARAM;
    }

	// We are on a 64-bit machine!  If this is 32-bit this won't work!
	long long sig1 = reinterpret_cast<long long> (handle);
	int fdesc = static_cast<int>(sig1);

    close(fdesc);

    return FMOD_OK;
}
	
// Check into FMOD_ERR_FILE_DISKEJECTED, from 
//	Set 'result' to the result expected from a normal file read callback.
//	If the read was successful, set it to FMOD_OK.
//	If it read some data but hit the end of the file, set it to FMOD_ERR_FILE_EOF.
//	If a bad error occurred, return FMOD_ERR_FILE_BAD
//	If a disk was ejected, return FMOD_ERR_FILE_DISKEJECTED.
//============================================================================================================================
FMOD_RESULT F_CALLBACK readnonblocking(void *handle, void *buffer, unsigned int sizebytes, unsigned int *bytesread, void *userdata) {
    if (!handle)
    {
        return FMOD_ERR_INVALID_PARAM;
    }

	//http://pulseaudio.org/wiki/SampleAsyncDeviceList -  PulseAudio flips out when NFS goes down, so we need to rewrite FMOD
	
	// We are on a 64-bit machine!  If this is 32-bit this won't work!
	long long sig1 = reinterpret_cast<long long> (handle);
	int fdesc = static_cast<int>(sig1);

	fd_set set;
	struct timeval timeout;
	FD_ZERO (&set);
	FD_SET (fdesc, &set);
	timeout.tv_sec = 0;
	timeout.tv_usec = 100;
	
	if (bytesread)
	{
		int selrtn = TEMP_FAILURE_RETRY(select(FD_SETSIZE, &set, NULL, NULL, &timeout));
		if (selrtn <= 0) {
			// 0 = Timeout, assume file unavailable, kill playback before buffer overruns start the skipping
			// -1 = error
			// 1 = input available
			return FMOD_ERR_FILE_EOF;
		}
		
        *bytesread = (int)read(fdesc, buffer, sizebytes);
    
        if (*bytesread < sizebytes)
        {
			LOG(CAT << "readnonblocking: " << strerror(errno));
            return FMOD_ERR_FILE_EOF;
        }
    }

    return FMOD_OK;
}
	
//============================================================================================================================
FMOD_RESULT F_CALLBACK seeknonblocking(void *handle, unsigned int pos, void *userdata)
{
    if (!handle)
    {
        return FMOD_ERR_INVALID_PARAM;
    }

	// We are on a 64-bit machine!  If this is 32-bit this won't work!
	long long sig1 = reinterpret_cast<long long> (handle);
	int fdesc = static_cast<int>(sig1);
    int err = lseek(fdesc, pos, SEEK_SET);
    
    if (err < 0) {
        LOG(CAT << "seeknonblocking: " << strerror(errno));
        return FMOD_ERR_FILE_EOF;
    }

    return FMOD_OK;
}

//============================================================================================================================
class FMODHandler {
public:
	static int		ignoreCallbackStop; // Set signal when user presses stop button AND a sound is playing, or user clicked on another song, implying STOP the currently playing song.  Must be turned off when read
	// Music manager
    FMOD::System    *system; // Playback system
    FMOD::System	*backSystem; // Processing system
    FMOD_RESULT      result;
    unsigned int     version;
    int              numdrivers;
    FMOD_SPEAKERMODE speakermode;
    FMOD_CAPS        caps;
    char             name[256];
    
    // For playback functions only
    FMOD::Channel	*channel;
	FMOD::Sound		*sound;
	FMOD_CREATESOUNDEXINFO soundInfo; // For FMOD
	static void (*handlerCallback)(int);

	float preMuteVolume;
	bool muted; // Defaults to not; Should we check system muted?
	

	// For analysis of non-playing sounds only (GetSample/StartSample/GetLength)
	FMOD::Sound		*soundAnalysis;
	FMOD_CREATESOUNDEXINFO soundInfoAnalysis;
	
	// Initialize our FMOD music manager, a freebie library requiring a dll, unfortunately.
	//============================================================================================================================
	FMODHandler() {
		handlerCallback = NULL;
		channel = NULL;
		sound = NULL;
		result = FMOD::System_Create(&system);
	    ERRCHECK(result);
	    
	    result = system->getVersion(&version);
	    ERRCHECK(result);
	
	    if (version < FMOD_VERSION) {
	        printf("Error!  You are using an old version of FMOD %08x.  This program requires %08x\n", version, FMOD_VERSION);
	    }
	    
	    result = system->getNumDrivers(&numdrivers);
	    ERRCHECK(result);
	
	    if (numdrivers == 0) {
	        result = system->setOutput(FMOD_OUTPUTTYPE_NOSOUND);
	        ERRCHECK(result);
	    }
	
	    result = system->init(32, FMOD_INIT_NORMAL, 0);
	    if (result == FMOD_ERR_OUTPUT_CREATEBUFFER) {
	        // Ok, the speaker mode selected isn't supported by this soundcard.  Switch it back to stereo... 
	        result = system->setSpeakerMode(FMOD_SPEAKERMODE_STEREO);
	        ERRCHECK(result);
	            
	        result = system->init(32, FMOD_INIT_NORMAL, 0);/* ... and re-init. */
	        ERRCHECK(result);
	    }
	 
		//result = system->setFileSystem(myopen, myclose, myread, myseek, /* NEED TO SET so we can notify of disk ejection or loss of NFS drive */0, 0, 2048);   
		result = system->setFileSystem(opennonblocking, closenonblocking, readnonblocking, seeknonblocking, 0, 0, 2048);   
		ERRCHECK(result);
		
	    system->setStreamBufferSize(10, FMOD_TIMEUNIT_MS); // Defaults to raw 16384 bytes, which is tooo slow)
        ERRCHECK(result);
	    
	    // Set up background processing (non-playing) system that can be reset at will
   		result = FMOD::System_Create(&backSystem);
	    ERRCHECK(result);
	    result = backSystem->init(32, FMOD_INIT_NORMAL, 0);
        ERRCHECK(result);
	    result = backSystem->setStreamBufferSize(10, FMOD_TIMEUNIT_MS); // Defaults to raw 16384 bytes, which is tooo slow)
        ERRCHECK(result);

	}
	
	//============================================================================================================================
	~FMODHandler() {
		system->release();
	}

	//============================================================================================================================
	//
	//  InitializeCallback
	//
	//  Set from main thread (jammit) to be called when the current song stops playing
	//  Pass a "1" to continue to the next song.
	//============================================================================================================================
	void InitializeCallback(void (*_handlerCallback)(int)) {
		if (_handlerCallback) {
			handlerCallback = _handlerCallback;
		} else {
			Exclamation("Invalid initializeCallback for FMOD");
		}
	}
	
	// In FMOD lib, paused songs are considered to be playing	
	//============================================================================================================================
	bool IsPlaying() {
		bool isPlaying;
		channel->isPlaying(&isPlaying);
		bool isPaused;
		channel->getPaused(&isPaused);
		
		// if paused, WE consider that Not playing
		if (isPaused) 
			return false;
		
		return isPlaying;
	}
	
	// Nice because then a play request doesn't have to determine what file to select or if they selected a song
	//============================================================================================================================
	bool IsPaused() {
		bool isPaused;
		channel->getPaused(&isPaused);
		return isPaused;
	}
	
	// If stopped, the control bar changes
	//============================================================================================================================
	bool IsStopped() {
		bool isPlaying;
		channel->isPlaying(&isPlaying);
		bool isPaused;
		channel->getPaused(&isPaused);
		return (!isPlaying && !isPaused);
	}

	//============================================================================================================================
	bool IsMuted() {
		float volume;
		channel->getVolume(&volume);
		
		// Let's assume that if the volume is 1 or 0 that its "muted"
		if ( volume <= .01f) {
			// We'll sync up the flag, though the preMuteLevel may be hosed
			muted = true;
			
		// Let's assume if the volume is over 75% then we are not muted, and sync the flag
		} else if (volume >= 0.75f ) {
			muted = false;
		}
		
		return muted;
	}

	// Let FMOD to check for callback states (dull polling method that is affected by latency)
	//============================================================================================================================
	void UpdateFMOD() {
		// TODO: Lock in case we are in shutdown
		system->update();
	}
	
	// Callback from PlayFile so delay is less noticable
	//============================================================================================================================
	//
	//  Play File - Called from MainWin.PlayFileCallback thread.
	//
	//============================================================================================================================
	int PlayFile(String filePath) {
		String UTFPath = ToSystemCharset(filePath); // REQUIRED to process UTF8 characters or else createStream will choke on some file paths

        bool isplaying = false;
        //FMOD_SOUND_TYPE  knownCodec; // Once I figure it out can save it in the database to speed loading
        
        memset(&soundInfo, 0, sizeof(FMOD_CREATESOUNDEXINFO));
        soundInfo.cbsize = sizeof(FMOD_CREATESOUNDEXINFO);
        //soundInfo.length = length;  // I don't want to calc the length since on mp3s it scans the file.
        //soundInfo.suggestedsoundtype = 0; //FMOD_SOUND_TYPE_MPEG; // Set Instead of scanning all codec types, use this to speed up loading by making it jump straight to this codec.
        int optimalDecodeBufferSize;
        
        if (CompareNoCase(filePath.Right(4), ".mp3")) {
            // mp3's can tolerate a very small buffer and start faster 
            // (100 may be too small and cause stuttering whenever CPU gets busy.  Happens when copy one is playing and U++ compile/links.
            // 200 I get no stutter, need to test delay
        	optimalDecodeBufferSize = 400;
        } else {
            optimalDecodeBufferSize = 0; // Use default 400 ms for other types such as flacs, which break horrendously on a smaller buffer
        }
    
        soundInfo.initialseekposition = 200; // skip in about 200 ms to reduce delay
        soundInfo.initialseekpostype = FMOD_TIMEUNIT_MS; // Should be faster than ms for mp3s
        //WString WfilePath = filePath; // Convert file to UTF so Open file will work for some bizarre files with special characters in the file name.  Verified that standard char strings fail for several songs.
		// TODO: put in a lock
		if (channel) {
			channel->stop();
		}
		//system->update();
		channel = NULL;
		if (sound) sound->release();
		sound = NULL;
		// TODO: createStream slooow, go to ASYNC
		
		result = system->setNetworkTimeout(100); // Try to get it to time out if NFS mounts fail
		ERRCHECK(result);
		
		result = system->createStream(UTFPath
		, FMOD_LOOP_OFF | FMOD_2D | FMOD_HARDWARE // This gives certain benefits such as EAX reverb, dolby digital output on some devices, and better 3d sound virtualization using headphones.
		  /* Do NOT use this tag: Prevents several mp3s from playing */ //| FMOD_IGNORETAGS // According to note in filecallbacks example, this prevents the player from seeking to the end for ID3V1 tags.  So turn them off: See what happens.
		, &soundInfo, &sound);
		// TODO: do non_blocking and check when loaded
		if (result != FMOD_OK) {
			// 25 may mean the file was so short it could not create a stream?
			LOG(CAT << " FMODHandler::PlayFile::Could not open " << UTFPath << ", Error is: " << FMOD_ErrorString(result));
			return -1; // Do not attempt to continue playing
		}
		//#define F_STDCALL _stdcall
		//#define F_CALLBACK F_STDCALL
		//typedef FMOD_RESULT (F_CALLBACK *FMOD_SYSTEM_CALLBACK)       (FMOD_SYSTEM *system, FMOD_SYSTEM_CALLBACKTYPE type, void *commanddata1, void *commanddata2);
		system->setCallback(FMODSystemCallback);
		result = system->playSound(FMOD_CHANNEL_FREE, sound, false, &channel);
		channel->setCallback(FMODChannelCallback);
		ERRCHECK(result);
		return 0;
		// TODO: Detect single-click selection; preload files on single click for speed
	}

	// Need way to get length that is accurate; tags incorrect or missing
	//============================================================================================================================
	unsigned int GetLength(String filePath) {
		String UTFPath = ToSystemCharset(filePath); // REQUIRED to process UTF8 characters or else createStream will choke on some file paths
        memset(&soundInfoAnalysis, 0, sizeof(FMOD_CREATESOUNDEXINFO));
        soundInfoAnalysis.cbsize = sizeof(FMOD_CREATESOUNDEXINFO);
		result = backSystem->createStream(UTFPath
		, FMOD_2D | FMOD_HARDWARE 
		| FMOD_ACCURATETIME // Needed to read time on VBR sources like MP3
		, &soundInfoAnalysis, &soundAnalysis);
	    if (result != FMOD_OK) {
	        if (result == FMOD_ERR_FORMAT) { // Ignore
	            return 0;
	    	}
			ERRLOG(result, filePath, (const char *)UTFPath);
			return 0;
		}
		unsigned int length;
		result = soundAnalysis->getLength(&length, FMOD_TIMEUNIT_MS);
		ERRLOG(result, filePath, (const char *)UTFPath);
		if (result != FMOD_OK) {
			if (soundAnalysis) {
				soundAnalysis->release();
			}
			return 0;
		}

		// Critical to memory management; if not released a CreateStream will return a 23 FILE NOT FOUND error, strangely		
		// Also will leak
		if (soundAnalysis) {
			soundAnalysis->release();
		}
		return length;
	}
	
	// NOTE that this function uses a different info buffer and sound pointer.  Do not want to collide with player
	//============================================================================================================================
	int StartSample(String filePath) {
		String UTFPath = ToSystemCharset(filePath); // REQUIRED to process UTF8 characters or else createStream will choke on some file paths
        memset(&soundInfoAnalysis, 0, sizeof(FMOD_CREATESOUNDEXINFO));
        soundInfoAnalysis.cbsize = sizeof(FMOD_CREATESOUNDEXINFO);
		result = system->createStream(UTFPath
		, FMOD_2D | FMOD_SOFTWARE // May be necessary to readdata
		| FMOD_OPENONLY // Open only, so as not to prefetch the buffer
		, &soundInfoAnalysis, &soundAnalysis);
		ERRCHECK(result);
		return 0;
	}
	
	//============================================================================================================================
	int GetSample(char *to, int len) {
		unsigned int read;

		result = soundAnalysis->readData((char *)to, len, &read);
		if (result == FMOD_ERR_FILE_EOF) {
			//sound->release();
			return 0;
		}
		
		ERRCHECK(result);	
		return read;	

	}
	
	//============================================================================================================================
	//
	//  FMODChannelCallback
	//
	//  The FMOD player calls here when the current song stops.  We let our client know so it can queue the next song.
	//============================================================================================================================
	
	static FMOD_RESULT F_CALLBACK FMODChannelCallback(
		FMOD_CHANNEL *               channel, 
		FMOD_CHANNEL_CALLBACKTYPE    type, 
		void *                       commanddata1, 
		void *                       commanddata2
	) {
		FMOD::Channel *cppchannel = (FMOD::Channel *)channel;
		int _ignoreCallbackStop = ignoreCallbackStop; // Necessary since U++ debugger won't show static values
//		Mutex::Lock __(playLock); // Prevent any other messages from being sent from main thread to the mp3 player
//		if (!activePlayer) { // Verify that a player has been assigned
    //		}

		// More code goes here.

		switch (type) {
		case FMOD_CHANNEL_CALLBACKTYPE_END:	
			// Problem:  SoundManager gets the information that the sound stopped and needs to update Main.  Need a callback.
			if (ignoreCallbackStop <= 0) { // Assuming that if it stopped on its own, then go on
				// Ignore for now til we figure this out
				
				if (handlerCallback) {
					(*handlerCallback)(1); // MainWin::SoundHandlerCallback ignores the actionType for now.
				}
				
				//activePlayer->SyncAction(WORKREQ_PLAYNEXT, REQSRC_SHUFFLEDIN);
			}
			LOG(CAT << "ignoreCallbackStop " << ignoreCallbackStop);
			// NOTE: This is the only place we swallow the signal up
			// Problem: MainWin access this ignore flag directly
			ignoreCallbackStop--; // Accept signal and disable.  Only REQSRC_GRID or MEDIACONTROL sets it.
			if (ignoreCallbackStop < 0)
				ignoreCallbackStop = 0;
			
			break;
			
		case FMOD_CHANNEL_CALLBACKTYPE_VIRTUALVOICE: // Called when a voice is swapped out or swapped in. 
		case FMOD_CHANNEL_CALLBACKTYPE_SYNCPOINT: // Called when a syncpoint is encountered. Can be from wav file markers. 
		case FMOD_CHANNEL_CALLBACKTYPE_OCCLUSION: // Called when the channel has its geometry occlusion value calculated. Can be used to clamp or change the value. 
			break;
		default:
			; // Error
		}
		return FMOD_OK;
	}


	//============================================================================================================================
	static FMOD_RESULT F_CALLBACK FMODSystemCallback(FMOD_SYSTEM *system, FMOD_SYSTEM_CALLBACKTYPE type, void *commanddata1, void *commanddata2) {
		FMOD::System *sys = (FMOD::System *)system;
		
		LOG(CAT << "FMODSystemCallback " << (int)type);
		
		
		switch (type) {
			case FMOD_SYSTEM_CALLBACKTYPE_DEVICELISTCHANGED: {
				int numdrivers;
				
				MYERR("NOTE: FMOD_SYSTEM_CALLBACKTYPE_DEVICELISTCHANGED occured.");
				
				sys->getNumDrivers(&numdrivers);
				
				MYERR(Format("Numdevices = %d", numdrivers));
				break;
			}
			case FMOD_SYSTEM_CALLBACKTYPE_MEMORYALLOCATIONFAILED: {
				MYERR(CAT 
					<< "ERROR: FMOD_SYSTEM_CALLBACKTYPE_MEMORYALLOCATIONFAILED occured. commanddata1=" 
					<< (long)commanddata1
					<< ", bytes=" << (long)commanddata2);
				break;
			}
			case FMOD_SYSTEM_CALLBACKTYPE_THREADCREATED: {
				MYERR(CAT << "NOTE: FMOD_SYSTEM_CALLBACKTYPE_THREADCREATED occured. Thread ID = " << (long)commanddata1);
				MYERR(Format("Thread Name = %s", (char *)commanddata2));
				break;
			}
			case FMOD_SYSTEM_CALLBACKTYPE_BADDSPCONNECTION:	{
				FMOD::DSP *source = (FMOD::DSP *)commanddata1;
				FMOD::DSP *dest = (FMOD::DSP *)commanddata2;
				
				MYERR("ERROR: FMOD_SYSTEM_CALLBACKTYPE_BADDSPCONNECTION occured.");
				if (source) {
					char name[256];
					source->getInfo(name, 0,0,0,0);
					MYERR(Format("SOURCE = %s", name));
				}
				if (dest) {
					char name[256];
					dest->getInfo(name, 0,0,0,0);
					MYERR(Format("DEST = %s", name));
				}
				break;
			}
		}
		
		return FMOD_OK;
	}

	//============================================================================================================================
	bool Mute() {
		// Trust the flag
		if (muted) {
			return false;
		}

		float volume; // 0.0 to 1.0
		channel->getVolume(&volume);
		preMuteVolume = volume;
		// Master volume is for ENTIRE computer, which is great.  Also very fast.  PlayerVolume is very slow.
		// If muted, volume will stay muted after exit of application.
		// TODO: When starting app; check if mastervolume is muted, if so, raise to some low level.
		channel->setVolume(0);
		muted = true;
		return true;
		// TODO: Override status to support a timeout, then return to a status; For instance "Unmute" is displayed for 2 seconds, then back to "Playing" or what have you.
		// TODO: Save volume to configuration for this user
	}
	
	//============================================================================================================================
	bool UnMute() {
		// Trust the flag
		if (!muted) {
			return false;
		}
		
		float volume;
		channel->getVolume(&volume);
		float newVolume = preMuteVolume;
		
		// If we are currently louder than the pre-mute saved volume, then do nothing
		newVolume = minmax(volume, preMuteVolume, 1.0f);
		
		channel->setVolume(newVolume);
		muted = false;
		return true;
	}


};


int FMODHandler::ignoreCallbackStop = 0;
void (*FMODHandler::handlerCallback)(int);

#endif

/*
FMOD_RESULT Sound::lock(
unsigned int  offset, 
unsigned int  length, 
void **  ptr1, 
void **  ptr2, 
unsigned int *  len1, 
unsigned int *  len2
);
result = system->createSound(0, FMOD_2D | FMOD_SOFTWARE | FMOD_OPENUSER, &exinfo, &sound);
ERRCHECK(result);

sound->lock(lastrecordpos * exinfo.numchannels * 2, blocklength * exinfo.numchannels * 2, &ptr1, &ptr2, &len1, &len2);   -- * exinfo.numchannels * 2 = stereo 16bit.  1 sample = 4 bytes. 
if (ptr1 && len1)
{
datalength += fwrite(ptr1, 1, len1, fp);
}
if (ptr2 && len2)
{
datalength += fwrite(ptr2, 1, len2, fp);
}

//Unlock the sound to allow FMOD to use it again.
sound->unlock(ptr1, ptr2, len1, len2);


result = 
//FMOD_RESULT Channel::getWaveData(
//  float *  wavearray, 
//  int  numvalues, 
//  int  channeloffset
//);
//

int             channels, bits;
float           rate;
void           *ptr1, *ptr2;
unsigned int    lenbytes, len1, len2;

if (!sound)
{
return;
}

sound->getFormat  (0, 0, &channels, &bits);
sound->getDefaults(&rate, 0, 0, 0);
sound->getLength  (&lenbytes, FMOD_TIMEUNIT_PCMBYTES);

//Lock the sound to get access to the raw data.
sound->lock(0, lenbytes, &ptr1, &ptr2, &len1, &len2);

//Write it to disk.
fwrite(ptr1, len1, 1, fp);


//    Unlock the sound to allow FMOD to use it again.

sound->unlock(ptr1, ptr2, len1, len2);

fclose(fp);
*/
